/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Charles Reich
 */

package redoak.php.bz2;

import redoak.php.bz2.impl.CBZip2InputStream;
import redoak.php.bz2.impl.CBZip2OutputStream;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.annotation.NotNull;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.annotation.ReturnNullAsFalse;
import com.caucho.quercus.env.ArrayValue;
import com.caucho.quercus.env.ArrayValueImpl;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.Value;
import com.caucho.quercus.lib.OutputModule;
import com.caucho.quercus.lib.file.BinaryInput;
import com.caucho.quercus.lib.file.BinaryOutput;
import com.caucho.quercus.lib.file.BinaryStream;
import com.caucho.quercus.lib.file.FileModule;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.util.L10N;
import com.caucho.vfs.TempBuffer;
import com.caucho.vfs.WriteStream;

/**
 * PHP Zlib
 */
public class Bz2Module extends AbstractQuercusModule {
  private static final Logger log
    = Logger.getLogger(Bz2Module.class.getName());
  private static final L10N L = new L10N(Bz2Module.class);

  public String []getLoadedExtensions()
  {
    return new String[] { "bz2" };
  }

  /**
   *
   * @param env
   * @param fileName
   * @param mode
   * @param useIncludePath always on
   * @return bzlib
   */
  @ReturnNullAsFalse
  public static BinaryStream bzopen(Env env,
				    StringValue fileName,
				    String mode,
				    @Optional("false") boolean useIncludePath)
  {
    String filemode = getFileMode(mode);
    int compressionLevel = getCompressionLevel(mode);

    Object val = FileModule.fopen(env, fileName, mode, useIncludePath, null);

    if (val == null)
      return null;

    try {
      int ch = filemode.charAt(0);

      if (ch == 'r') {
        BinaryInput is = (BinaryInput) val;
        return new Bz2FileInputStream(env, is);
      }
      else if (ch == 'w') {
        return new Bz2FileOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel);
      }
      else if (ch == 'a') {
        return new Bz2FileOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel);
      }
      else if (ch == 'x') {
        return new Bz2FileOutputStream(((BinaryOutput) val).getOutputStream(),
				    compressionLevel);
      }
    }
    catch (IOException e) {
      log.log(Level.FINE, e.getMessage(), e);
      env.warning(L.l(e.getMessage()));
    }

    return null;
  }

  /**
   *
   * @param env
   * @param fileName
   * @param useIncludePath
   * @return array of uncompressed lines from fileName
   */
  @ReturnNullAsFalse
  public static ArrayValue bzfile(Env env,
				  StringValue fileName,
				  @Optional boolean useIncludePath)
  {
    BinaryInput is = (BinaryInput) bzopen(env, fileName, "r", useIncludePath);

    if (is == null)
      return null;

    try {
      ArrayValue result = new ArrayValueImpl();

      StringValue line;
      while ((line = is.readLine(Integer.MAX_VALUE)) != null &&
	     line.length() > 0)
	result.put(line);

      return result;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      is.close();
    }
  }

  /**
   * outputs uncompressed bytes directly to browser, writes a warning message
   *   if an error has occured
   * Note: PHP5 is supposed to print an error message but it doesn't do it
   *
   * @param env
   * @param fileName
   * @param useIncludePath
   * @return number of bytes read from file, or FALSE if an error occurred
   */
  public static Value readbzfile(Env env,
				 StringValue fileName,
				 @Optional boolean useIncludePath)
  {
    BinaryInput is = (BinaryInput) bzopen(env, fileName, "r", useIncludePath);

    if (is == null)
      return BooleanValue.FALSE;

    try {
      return LongValue.create(env.getOut().writeStream(is.getInputStream()));
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      is.close();
    }
  }

  /**
   * Writes a string to the gzip stream.
   */
  public static int bzwrite(@NotNull BinaryOutput os,
			    InputStream is,
			    @Optional("0x7fffffff") int length)
  {
    if (os == null)
      return 0;

    try {
      return os.write(is, length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   *
   * @param env
   * @param zp
   * @param s
   * @param length
   * @return alias of gzwrite
   */
  public int bzputs(Env env,
                    @NotNull BinaryOutput os,
                    InputStream is,
                    @Optional("0x7ffffff") int length)
  {
    if (os == null)
      return 0;

    try {
      return os.write(is, length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Closes the stream.
   */
  public boolean bzclose(@NotNull BinaryStream os)
  {
    if (os == null)
      return false;
    
    os.close();

    return true;
  }

  /**
   * Returns true if the GZip stream is ended.
   */
  public boolean bzeof(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return true;

    return binaryStream.isEOF();
  }

  /**
   * Reads a character from the stream.
   */
  public static Value bzgetc(Env env, @NotNull BinaryInput is)
  {
    if (is == null)
      return BooleanValue.FALSE;

    try {
      int ch = is.read();

      if (ch < 0)
        return BooleanValue.FALSE;
      else {
	StringValue sb = env.createBinaryBuilder(1);

	sb.appendByte(ch);
	
        return sb;
      }
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Reads a chunk of data from the gzip stream.
   */
  @ReturnNullAsFalse
  public StringValue bzread(@NotNull BinaryInput is, int length)
  {
    if (is == null)
      return null;

    try {
      return is.read(length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Reads a line from the input stream.
   */
  public static Value bzgets(Env env,
			      @NotNull BinaryInput is,
			      int length)
  {
    return FileModule.fgets(env, is, length);
  }

  /**
   * Reads a line from the zip stream, stripping tags.
   */
  public static Value bzgetss(Env env,
                              @NotNull BinaryInput is,
                              int length,
                              @Optional Value allowedTags)
  {
    return FileModule.fgetss(env, is, length, allowedTags);
  }

  /**
   * Rewinds the stream to the very beginning
   */
  public boolean bzrewind(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return false;

    return binaryStream.setPosition(0);
  }

  /**
   * Set stream position to the offset
   * @param offset absolute position to set stream to
   * @param whence if set, changes the interpretation of offset like fseek
   * @return 0 upon success, else -1 for error
   */
  public int bzseek(@NotNull BinaryStream binaryStream,
                    long offset,
                    @Optional("FileModule.SEEK_SET") int whence)
  {
    if (binaryStream == null)
      return -1;

    if (binaryStream.seek(offset, whence) == -1)
      return -1;

    return 0;
  }

  /**
   * Gets the current position in the stream
   * @return the position in the stream, or FALSE for error
   */
  public Value bztell(@NotNull BinaryStream binaryStream)
  {
    if (binaryStream == null)
      return BooleanValue.FALSE;
    return new LongValue(binaryStream.getPosition());
  }

  /**
   * Prints out the remaining data in the stream to stdout
   */
  public Value bzpassthru(Env env, @NotNull BinaryInput is)
  {
    WriteStream out = env.getOut();
    TempBuffer tempBuf = TempBuffer.allocate();
    byte[] buffer = tempBuf.getBuffer();

    int length = 0;
    try {
      int sublen = is.read(buffer, 0, buffer.length);
      while (sublen > 0) {
        out.write(buffer, 0, sublen);
        length += sublen;
        sublen = is.read(buffer, 0, buffer.length);
      }

      return new LongValue(length);

    } catch (IOException e) {
      log.log(Level.FINE, e.toString(), e);

      return BooleanValue.FALSE;

    } finally {
      TempBuffer.free(tempBuf);
    }
  }


  /**
   * compresses data using zlib
   *
   * @param data
   * @param level (default is Deflater.DEFAULT_COMPRESSION)
   * @return compressed string
   */
  public Value bzcompress(Env env,
			  InputStream data,
                          @Optional("6") int level)
  {
	    
	    try 
	    {
	    	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	    	OutputStream bz = new CBZip2OutputStream(baos, level);
	    	int b;
	    	while((b = data.read())>=0)
	    	{
	    		bz.write(b);
	    	}
	    	bz.flush();
	    	bz.close();
	    	StringValue out = env.createLargeBinaryBuilder();
	    	out.append(baos.toByteArray());

	    	return out;
	    } catch (Exception e) {
	      throw QuercusModuleException.create(e);
	    }
  }

  /**
   *
   * @param data
   * @param length (maximum length of string returned)
   * @return uncompressed string
   */
  public Value bzuncompress(Env env,
			    InputStream is,
                            @Optional("0") long length)
  {
	    
	    try 
	    {
	    	StringValue out = env.createLargeBinaryBuilder();
	    	InputStream bz = new CBZip2InputStream(is);
	    	int b;
	    	while((b = bz.read())>=0)
	    	{
	    		out.appendByte(b);
	    	}
	    	try {is.close();} catch (Exception e) {}
	    	return out;
	    } catch (Exception e) {
	      throw QuercusModuleException.create(e);
	    }
  }
  

  /**
   * Helper function to retrieve the filemode closest to the end
   * Note: PHP5 unexpectedly fails when 'x' is the mode.
   *
   * XXX todo: toss a warning if '+' is found
   *    (bzip cannot be open for both reading and writing at the same time)
   *
   */
  private static String getFileMode(String input)
  {
    String modifier = "";
    String filemode = input.substring(0, 1);

    for (int i = 1; i < input.length(); i++ )
    {
      char ch = input.charAt(i);
      switch (ch) {
        case 'r':
          filemode = "r";
          break;
        case 'w':
          filemode = "w";
          break;
        case 'a':
          filemode = "a";
          break;
        case 'b':
          modifier = "b";
          break;
        case 't':
          modifier = "t";
          break;
      }
    }
    return filemode + modifier;
  }

  /**
   * Helper function to retrieve the compression level
   *     - finds the compression level nearest to the end and returns that
   */
  private static int getCompressionLevel(String input)
  {
    for (int i = input.length() - 1; i >= 0; i--) {
      char ch = input.charAt(i);
      
      if (ch >= '0' && ch <= '9')
        return ch - '0';
    }
    
    return 6;
  }

}
